﻿#region Using Directives

using System.Collections.Generic;
using System.Text;
using System.Xml;

#endregion Using Directives

namespace SharePointStu.CAMLHelper.CAML
{
    /// <summary>
    /// CAML manager class for generating CAML queries.
    /// </summary>
    public sealed class CAMLManager
    {
        #region Private Members

        const string ELEMENT_QUERY = "Query";
        const string ELEMENT_WHERE = "Where";
        const string ELEMENT_ORDERBY = "OrderBy";
        const string ELEMENT_VIEWFIELDS = "ViewFields";
        const string ELEMENT_ROWLIMIT = "RowLimit";

        List<QueryGroup> _queryGroups;
        List<OrderBy> _orderBy;
        List<ViewField> _viewFields;
        Types.JoinTypes _queryGroupsOuterJoinType;
        int _rowLimit;

        #endregion Private Members

        #region Public Properties

        /// <summary>
        /// Get or set the generic list of <see cref="SharePointStu.CAMLHelper.CAML.QueryGroup"/> objects.
        /// </summary>
        public List<QueryGroup> QueryGroups
        {
            get { return _queryGroups; }
            set { _queryGroups = value; }
        }

        /// <summary>
        /// Get or set the top level <see cref="SharePointStu.CAMLHelper.CAML.Types.JoinTypes"/> element to use
        /// when there are multiple root types.
        /// Example: &lt;OR&gt;...&lt;/OR&gt;&lt;AND&gt;...&lt;/AND&gt;
        /// This is achieved by adding queries and specifing a <see cref="SharePointStu.CAMLHelper.CAML.QueryGroup.MergeTypes"/>
        /// value of TopGroup or BottomGroup.
        /// This property will be ignored if there is only one top level CAML element within the Where clause.
        /// Default value is Or.
        /// </summary>
        public Types.JoinTypes QueryGroupsOuterJoinType
        {
            get { return _queryGroupsOuterJoinType; }
            set { _queryGroupsOuterJoinType = value; }
        }

        /// <summary>
        /// Get or set the generic list of <see cref="SharePointStu.CAMLHelper.CAML.OrderBy"/> objects.
        /// </summary>
        public List<OrderBy> OrderBy
        {
            get { return _orderBy; }
            set { _orderBy = value; }
        }

        /// <summary>
        /// Get or set the generic list of <see cref="SharePointStu.CAMLHelper.CAML.ViewField"/> objects.
        /// </summary>
        public List<ViewField> ViewFields
        {
            get { return _viewFields; }
            set { _viewFields = value; }
        }

        /// <summary>
        /// Get or set the row limit value for CAML query results.
        /// </summary>
        public int RowLimit
        {
            get { return _rowLimit; }
            set { _rowLimit = value; }
        }

        #endregion Public Properties

        #region Constructors

        /// <summary>
        /// Initialise the <see cref="T:SharePointStu.CAMLHelper.CAML.CAMLManager"/> object.
        /// </summary>
        public CAMLManager()
        {
            _queryGroups = new List<QueryGroup>();
            _orderBy = new List<OrderBy>();
            _viewFields = new List<ViewField>();
            _queryGroupsOuterJoinType = Types.JoinTypes.Or;
        }

        /// <summary>
        /// Initialise the <see cref="T:SharePointStu.CAMLHelper.CAML.CAMLManager"/> object.
        /// </summary>
        /// <param name="queryGroup">The <see cref="T:SharePointStu.CAMLHelper.CAML.QueryGroup"/> object to initialise with.</param>
        public CAMLManager(QueryGroup queryGroup)
            : this()
        {
            _queryGroups.Add(queryGroup);
        }

        /// <summary>
        /// Initialise the <see cref="T:SharePointStu.CAMLHelper.CAML.CAMLManager"/> object.
        /// </summary>
        /// <param name="order">The <see cref="T:SharePointStu.CAMLHelper.CAML.OrderBy"/> object to initialise with.</param>
        public CAMLManager(OrderBy order)
            : this()
        {
            _orderBy.Add(order);
        }

        /// <summary>
        /// Initialise the <see cref="T:SharePointStu.CAMLHelper.CAML.CAMLManager"/> object.
        /// </summary>
        /// <param name="queryGroup">The <see cref="T:SharePointStu.CAMLHelper.CAML.QueryGroup"/> object to initialise with.</param>
        /// <param name="order">The <see cref="T:SharePointStu.CAMLHelper.CAML.OrderBy"/> object to initialise with.</param>
        public CAMLManager(QueryGroup queryGroup, OrderBy order)
            : this()
        {
            _queryGroups.Add(queryGroup);
            _orderBy.Add(order);
        }

        #endregion Constructors

        #region Public Methods

        /// <summary>
        /// Get the XML CAML string for the current collection of <see cref="T:SharePointStu.CAMLHelper.CAML.QueryGroup"/>
        /// and <see cref="T:SharePointStu.CAMLHelper.CAML.OrderBy"/> objects.
        /// </summary>
        /// <returns>XML CAML query string.</returns>
        public string GetCAML()
        {
            XmlDocument doc = new XmlDocument();
            XmlElement queryEl = doc.CreateElement(ELEMENT_QUERY);
            XmlElement whereEl = doc.CreateElement(ELEMENT_WHERE);

            doc.AppendChild(queryEl);
            queryEl.AppendChild(whereEl);

            for (int ii = 0; ii < _queryGroups.Count; ii++)
            {
                QueryGroup item = _queryGroups[ii];
                if (item.Query != null)
                {
                    string caml = item.Query.GetCAML();

                    switch (ii)
                    {
                        case 0: // Ignore join type for query group as we only have one query so far.
                            whereEl.InnerXml = caml;
                            break;
                        case 1: // Ignore join type property as second query must be contained within the same group as the first item.
                            XmlElement joinEl = doc.CreateElement(item.JoinType.ToString());
                            joinEl.InnerXml = whereEl.InnerXml + caml;
                            whereEl.InnerXml = joinEl.OuterXml;
                            break;
                        default:
                            if (item.MergeType == QueryGroup.MergeTypes.TopGroup || item.MergeType == QueryGroup.MergeTypes.BottomGroup)
                                AddGrouping(whereEl, item);
                            else
                                whereEl.InnerXml = string.Format("<{0}>{1}{2}</{0}>", item.JoinType.ToString(), whereEl.InnerXml, caml);
                            break;
                    }
                }
            }

            if (whereEl.ChildNodes.Count > 1)
            {
                XmlElement outerElement = doc.CreateElement(QueryGroupsOuterJoinType.ToString());
                outerElement.InnerXml = whereEl.InnerXml;
                whereEl.InnerXml = outerElement.OuterXml;
            }

            // Get the order by clause for the query.
            XmlDocument orderDoc = GetOrderBy();
            if (orderDoc != null && orderDoc.ChildNodes.Count > 0)
            {
                XmlNode node = doc.ImportNode(orderDoc.ChildNodes[0], true);
                queryEl.AppendChild(node);
            }

            if (doc.DocumentElement != null)
            {
                return doc.DocumentElement.InnerXml;
            }

            return string.Empty;
        }

        /// <summary>
        /// Get a <see cref="T:System.Xml.XmlDocument"/> object containing the CAML query for the current
        /// collection of <see cref="T:SharePointStu.CAMLHelper.CAML.QueryGroup"/> and <see cref="T:SharePointStu.CAMLHelper.CAML.OrderBy"/> objects.
        /// </summary>
        /// <returns>A <see cref="T:System.Xml.XmlDocument"/> object containing the CAML query.</returns>
        public XmlDocument GetCAMLAsXML()
        {
            XmlDocument doc = new XmlDocument();
            string caml = GetCAML();
            doc.LoadXml(caml);

            return doc;
        }

        /// <summary>
        /// Get the XML CAML string for the current collection of <see cref="T:SharePointStu.CAMLHelper.CAML.QueryGroup"/>
        /// and <see cref="T:SharePointStu.CAMLHelper.CAML.OrderBy"/> objects, plus the ViewField and RowLimit elements.
        /// </summary>
        /// <remarks>The ViewField and RowLimit elements will not be returned if no valid values exist.</remarks>
        /// <returns>Full XML CAML query string.</returns>
        public string GetFullCAML()
        {
            string caml = GetCAML();
            string view = GetViewFields();
            string rowLimit = GetRowLimit();

            return caml + view + rowLimit;
        }

        /// <summary>
        /// Get a <see cref="T:System.Xml.XmlDocument"/> object containing the CAML query for the current
        /// collection of <see cref="T:SharePointStu.CAMLHelper.CAML.QueryGroup"/> and <see cref="T:SharePointStu.CAMLHelper.CAML.OrderBy"/>
        /// objects, plus the ViewField and RowLimit elements.
        /// </summary>
        /// <remarks>The ViewField and RowLimit elements will not be returned if no valid values exist.</remarks>
        /// <returns>A <see cref="T:System.Xml.XmlDocument"/> object containing the full CAML query.</returns>
        public XmlDocument GetFullCAMLAsXML()
        {
            string caml = GetCAML();
            string view = GetViewFields();
            string rowLimit = GetRowLimit();
            string xml = caml + view + rowLimit;

            XmlDocument doc = new XmlDocument();
            doc.LoadXml(xml);

            return doc;
        }

        /// <summary>
        /// Get the XML CAML string containing the ViewFields section for the CAML query.
        /// </summary>
        /// <returns>XML string of ViewField nodes.</returns>
        public string GetViewFields()
        {
            XmlDocument doc = new XmlDocument();
            StringBuilder sb = new StringBuilder();

            if (ViewFields.Count > 0)
            {
                sb.Append(string.Format("<{0}>", ELEMENT_VIEWFIELDS));
                foreach (ViewField view in ViewFields)
                {
                    sb.Append(view.GetCAML());
                }
                sb.Append(string.Format("</{0}>", ELEMENT_VIEWFIELDS));
            }

            if (sb.Length > 0)
                doc.LoadXml(sb.ToString());

            return doc.OuterXml;
        }

        /// <summary>
        /// Get the XML CAML string containing the RowLimit section of the CAML query.
        /// </summary>
        /// <returns>XML string for the RowLimit node.</returns>
        public string GetRowLimit()
        {
            string result = string.Empty;

            if (RowLimit > 0)
            {
                result = string.Format("<{0}>", ELEMENT_ROWLIMIT);
                result += RowLimit.ToString();
                result += string.Format("</{0}>", ELEMENT_ROWLIMIT);
            }

            return result;
        }

        #endregion Public Methods

        #region Private Methods

        /// <summary>
        /// Add a <see cref="T:SharePointStu.CAMLHelper.CAML.QueryGroup"/> object to the CAML group as represented by the container
        /// XmlElement parameter.
        /// </summary>
        /// <param name="container">The XmlElement that represents either CAML section to add the
        /// current <see cref="T:SharePointStu.CAMLHelper.CAML.QueryGroup"/> object to.</param>
        /// <param name="item">The <see cref="T:SharePointStu.CAMLHelper.CAML.QueryGroup"/> object to add to the CAML query using
        /// the container XmlElement parameter.</param>
        private void AddGrouping(XmlElement container, QueryGroup item)
        {
            if (item.MergeType == QueryGroup.MergeTypes.Contain)
                return;

            if (container != null && container.OwnerDocument != null)
            {
                int index = item.MergeType == QueryGroup.MergeTypes.TopGroup ? 0 : 1;
                XmlElement joinEl = container.OwnerDocument.CreateElement(item.JoinType.ToString());
                string caml = item.Query.GetCAML();

                if (container.ChildNodes.Count == 1)
                {
                    container.InnerXml += caml;
                    QueryGroupsOuterJoinType = item.JoinType;
                }
                else
                {
                    // If the group's join node is different to the new item's join type
                    // then make the new item the parent of the group.
                    if (container.ChildNodes[index].Name != item.JoinType.ToString())
                    {
                        joinEl.InnerXml = container.ChildNodes[index].OuterXml + caml;
                        container.RemoveChild(container.ChildNodes[index]);

                        switch (item.MergeType)
                        {
                            case QueryGroup.MergeTypes.TopGroup:
                                container.PrependChild(joinEl);
                                break;
                            case QueryGroup.MergeTypes.BottomGroup:
                                container.AppendChild(joinEl);
                                break;
                        }
                    }
                    else
                    {
                        if (container.ChildNodes[index].ChildNodes.Count == 1)
                            container.ChildNodes[index].InnerXml += caml;
                        else
                            container.ChildNodes[index].InnerXml = string.Format("<{0}>{1}</{0}>{2}",
                                                                                 item.JoinType.ToString(),
                                                                                 container.ChildNodes[index].InnerXml,
                                                                                 caml);
                    }
                }
            }
        }

        /// <summary>
        /// Get an XmlDocument containing the OrderBy section of the CAML query.
        /// </summary>
        /// <returns>XmlDocument of OrderBy nodes.</returns>
        private XmlDocument GetOrderBy()
        {
            XmlDocument doc = new XmlDocument();
            StringBuilder sb = new StringBuilder();

            if (OrderBy.Count > 0)
            {
                sb.Append(string.Format("<{0}>", ELEMENT_ORDERBY));
                foreach (OrderBy order in OrderBy)
                {
                    sb.Append(order.GetCAML());
                }
                sb.Append(string.Format("</{0}>", ELEMENT_ORDERBY));
            }

            if (sb.Length > 0)
                doc.LoadXml(sb.ToString());

            return doc;
        }

        #endregion Private Methods
    }
}
